/**
* @name HiveMaps
* @version 0.9.2
* @author Joe Johnston <joe@socialhive.org>
* @copyright (c) 2009 Joe Johnston
* http://socialhive.org/hivemaps
* http://code.google.com/p/hivemaps
*
* Creates SOCIALHIVE.map
*
* Requires hiveutils, hivegeocoder
*
**/

/**
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/

/**
* TODO: figure out why continuously scrolling the map left or right does not automatically update point
* locations when they scroll offscreen.  Seems like a bug in Google Maps.  Maybe needs a manual refresh.
**/

(function($) {
  function map(mapElem, options) {
    this.options = $.extend(HiveMap.defaultOptions, options);
    this.mapDiv = (typeof mapElem == 'string') ? $('#'+mapElem)[0] : mapElem;
    this.map = new GMap2(this.mapDiv);
    this.data = null;
    this.markers = [];
    this.markersLoaded = false;
    this.markersInView = [];
    this.markerClusterer = null;
    this.timers = {};
     
    this.map.addControl(new Logo);
       
    if (this.options.listDiv && typeof this.options.listDiv == 'string')
      this.options.listDiv = $('#'+this.options.listDiv)[0];
                
    this.loadData = function(feed, type, schema){
      return loadData(this, feed, type, schema);
    };
    this.initMarkers = function(data){
      return initMarkers(this, data);
    };
    this.search = function(query, successCallback, errorCallback){
      return searchMap(this, query, successCallback, errorCallback);
    };
    this.zoom = function(zoom, lat, lon){
      return updateMap(this, lat, lon, zoom);
    };
    this.pan = function(lat, lon, zoom){
      return updateMap(this, lat, lon, zoom);
    };
    this.getLinkQuery = function(){
      return $.address.value();
    };
    this.getLink = function(){
      return $.address.baseURL()+'#'+$.address.value();
    };
    this.fitToMarkers = function(markers, maxZoom){
      return fitToMarkers(this, markers, maxZoom);
    };
    
    if (this.options.onMapInit)
      this.options.onMapInit(this);
    if (this.options.enableHistory)
      this.options.initHistory(this);
    var map = this;
    GEvent.addListener(this.map, 'moveend', function(){map.options.onMapMoved(map);});
    (this.options.dataFeed) ?
      this.loadData(this.options.dataFeed, this.options.dataType) :
      setTimeout(function(){map.options.hideLoading(map);}, 100);  
  }

  var HiveMap = {
    createMap: function(mapID, options) {
      return new map(mapID, options);  
    },

    // error codes
    ERROR_LOAD_DATA: 'ERROR_LOAD_DATA',
    ERROR_NO_GEOCODER: 'ERROR_NO_GEOCODER',
    ERROR_SEARCH_FAILED: 'ERROR_SEARCH_FAILED',
    
    // default map options
    defaultOptions: {
      debug: false,
      // max number of loop iterations per batch; setting this too large might cause script timeouts
      loopBatchSize: 200,
      // geocoder automatically defaults to SOCIALHIVE.geo.geocoder
      geocoder: null,
      enableHistory: true,
      initHistory: initHistory,
      historyName: 'hivemap',
      defaultZoom: 1,
      defaultCenter: {lat:25.8, lon:13.36},
      maxMarkers: 5000,
      maxListSize: 500,
      checkMarkerOverlap: checkMarkerOverlap,
      loadingBar: LoadingBar,
      dataFeed: null,
      // dataType: xml or json
      dataType: 'xml',
      dataSchema: {
        // root: node of markers list
        root: 'xml',
        // marker: schema object of node names
        marker: {
          title: 'title',
          lat: 'lat',
          lon: 'lon'
        }
      },
      listDiv: null,
      markerFunc: 'HiveMapsMarker',
      markerClusterer: 'HiveMapsClusterer',
      imagesBase: 'images/',
      markerClustererOptions: {
        gridSize: 55,
        maxZoom: 11,
        styles: [
          {url:'marker2.png', width:53, height:53, opt_textColor:'#FFF'},
          {url:'marker3.png', width:56, height:56, opt_textColor:'#FFF'},
          {url:'marker4.png', width:66, height:66, opt_textColor:'#FFF'},
          {url:'marker4.png', width:66, height:66, opt_textColor:'#FFF'},
          {url:'marker4.png', width:66, height:66, opt_textColor:'#FFF'}
        ]
      },
      /**
      * Dictionary of icon options objects or GIcon instances
      *
      * If the icon property is present in the loaded data, the cooresponding icon will be used if found in 
      * markerIcons.  Otherwise, the 'default' entry will be used.
      **/
      markerIcons: {
        // default: new GIcon(G_DEFAULT_ICON)
        'default': {
          icon: {url:'marker1.png', width:18, height:18},
          shadow: null,
          iconAnchor: {x:9, y:9},
          infoWindowAnchor: {x:9, y:9},
          transparent: 'marker_trans.png'
        }
      },
      messages: {
        errors: {
          ERROR_LOAD_DATA: 'Unable to load map marker data.',
          ERROR_NO_GEOCODER: 'Geocoder not specified.',
          ERROR_SEARCH_FAILED: 'Unable to find location.'
        }
      },
      gettext: function(str) {
        return str;
      },
      listItemHTML: function(map, marker){
        return '<div><a href="'+marker.data.id+'">'+marker.data[map.options.dataSchema.marker.title]+'</a></div>';
      },
      moreListItemsHTML: function(map, count, list){
        return '<div>'+(list.length-count)+' '+map.options.gettext('points not shown')+'.  '+map.options.gettext('Zoom in on the map')+'.</div>';
      },
      showLoading: function(map){
        if (map.options.loadingBar) {
          if (typeof map.options.loadingBar != 'object')
            map.options.loadingBar = new map.options.loadingBar;
          map.map.addControl(map.options.loadingBar);
        }
      },
      hideLoading: function(map){
        if (typeof map.options.loadingBar == 'object')
          map.map.removeControl(map.options.loadingBar);
        if (typeof map.options.onReady == 'function')
          map.options.onReady(map);
      },
      raiseError: function(error) {
        alert(error.map.options.gettext('Error')+': '+error.map.options.gettext(error.message));
      },
      // called when map is ready for interaction
      onReady: function(map){
      },
      // called when data feed is done loading and ready for processing
      onDataLoad: function(map, data){
        if (map.options.dataType == 'xml')
          data = SOCIALHIVE.utils.xml2js(data, map.options.dataSchema.root);
        map.data = data;
        map.initMarkers(data);
      },
      // called when map is initialized but before any markers are loaded
      onMapInit: function(map){
        map.options.showLoading(map);
        map.map.setCenter(new GLatLng(map.options.defaultCenter.lat, map.options.defaultCenter.lon), map.options.defaultZoom);
        map.map.addControl(new GMapTypeControl());
        map.map.removeMapType(G_SATELLITE_MAP);
        map.map.addMapType(G_PHYSICAL_MAP);
        map.map.addControl(new GLargeMapControl());
        map.map.disableScrollWheelZoom();
        map.map.setMapType(G_HYBRID_MAP); 
      },
      onMarkersInit: function(map){
        (map.options.markerClusterer) ? setTimeout(function(){initMarkerClusterer(map);}, 100) : addMarkers(map, map.markers);
        (map.options.listDiv) ? setTimeout(function(){initList(map);}, 100) : setTimeout(function(){map.options.hideLoading(map);}, 100); 
      },
      onListInit: function(map, list, max){
      },
      onMapMoved: function(map) {
        if (map.options.listDiv && map.markersLoaded) {
          map.options.showLoading(map);
          setTimeout(function(){initList(map);}, 100);
        }
      },
      onDoneDrawing: function(map) {
        map.options.hideLoading(map);
      },
      // called when a non-overlapping marker is clicked
      onMarkerClick: function(map, marker) {
        map.map.openInfoWindowHtml(marker.getLatLng(), marker.data[map.options.dataSchema.marker.title]);
      },
      // called when a marker is clicked that is overlapping other markers at the current zoom level
      onMarkerClickOverlap: function(map, marker, list) {
        var node = $(document.createElement('ol')).attr('id', 'infoMarkerList');
        $(list).each(function(i, m){
          $('<li><a href="#">'+m.data[map.options.dataSchema.marker.title]+'</a></li>').click(function(){map.options.onMarkerClick(map, m); return false;}).appendTo(node);
        });
        map.map.openInfoWindow(marker.getLatLng(), node[0]);        
      }
    }
  };

  
  function LoadingBar() {
    this.container = null;
  }
  LoadingBar.prototype = new GControl();
  LoadingBar.prototype.initialize = function(map) {
    this.container = $(document.createElement('div')).attr('class', 'loading').appendTo(map.getContainer());
    return this.container[0];
  };
  LoadingBar.prototype.getDefaultPosition = function() {
    return new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(90, 10));
  };

  function Logo() {
    this.container = null;
  }
  Logo.prototype = new GControl();
  Logo.prototype.initialize = function(map) {
    
    this.container = $(document.createElement('div')).css({
      'background': 'url(http://hivedata.appspot.com/static/img/1/socialhive_logo.png) 0 0 no-repeat',
      'width': '62px',
      'height': '29px',
      'cursor': 'pointer'
    }).bind('mouseover', function(e){
      $(this).css('background-position', '-66px 0');
    }).bind('mouseout', function(e){
      $(this).css('background-position', '0 0');
    }).bind('click', function(e){
      window.location='http://socialhive.org';
    }).appendTo(map.getContainer());
    return this.container[0];
  };
  Logo.prototype.getDefaultPosition = function() {
    return new GControlPosition(G_ANCHOR_BOTTOM_LEFT, new GSize(66, 6));
  };


  function initMarkerClusterer(map) {
    if (!map.options.markerClustererOptions._imagesBaseAdded) {
      var styles = map.options.markerClustererOptions.styles;
      for (var i=0; i<styles.length; i++)
        styles[i].url = map.options.imagesBase+styles[i].url;
      map.options.markerClustererOptions._imagesBaseAdded = true;
    }
    // todo: cleanup markerClusterer if it already exists before creating a new one
    map.markerClusterer = new window[map.options.markerClusterer](map.map, map.markers, map.options.markerClustererOptions);
    map.markerClusterer.onDoneDrawing = function(){map.options.onDoneDrawing(map)};
    map.markerClusterer.debug = map.options.debug;
  }
  
  function initList(map) {
    list = getMarkersInView(map);
    var max = (map.options.maxListSize < list.length) ? map.options.maxListSize : list.length;
    map.options.onListInit(map, list, max);
    ma = [];
    var htmlfunc = map.options.listItemHTML;
    var title = map.options.dataSchema.marker.title;
    for (var i=0; i<max; i++) {
      if (!list[i].data[title])
        continue;
      ma.push(htmlfunc(map, list[i]));
    }
    if (i < list.length)
      ma.push(map.options.moreListItemsHTML(map, i, list));
    setTimeout(function(){map.options.listDiv.innerHTML = ma.join('');}, 50);
    setTimeout(function(){map.options.hideLoading(map);}, 500);
  }
  
  function initHistory(map) {
    GEvent.addListener(map.map, 'moveend', function(){updateHistory(map);});
    $.address.change(function(e){
      var history = e.pathNames;
      if (history[0] != map.options.historyName || history.length < 4)
        return false;
      var center = map.map.getCenter();
      if (center.lat()!=history[1] || center.lng()!=history[2] || map.map.getZoom()!=history[3]) {
        updateMap(map, history[1], history[2], history[3]);
      }
    });
  }

  function initMarkers(map, data) {
    // if initMarkers loop is already in progress, stop it, wait a bit for the current 
    // loop to exit and try again 
    if (map.timers.initMarkers) {
      clearTimeout(map.timers.initMarkers);
      delete map.timers.initMarkers;
      setTimeout(function(){initMarkers(map, data);}, 200);
      return;
    }
    map.map.clearOverlays();
    map.markers = [];
    if (!map.options.markerIcons)
      map.options.markerIcons = {};       
    if (map.markerClusterer)
      map.markerClusterer.clearMarkers();
    var opt = {
      markerfunc: (window[map.options.markerFunc] || GMarker),
      clickfunc: function(){(map.options.checkMarkerOverlap) ? map.options.checkMarkerOverlap(map, this) : map.options.onMarkerClick(map, this);},
      max: (map.options.maxMarkers < data.length) ? map.options.maxMarkers : data.length,
      lat: map.options.dataSchema.marker.lat,
      lon: map.options.dataSchema.marker.lon
    };
    map.timers.initMarkers = setTimeout(function(){_initMarkers(map, data, opt, 0, map.options.loopBatchSize);}, 10);
  }

  function _initMarkers(map, data, opt, batchNum, batchSize){
    var cnt = batchNum*batchSize;
    var n = (cnt+batchSize > opt.max) ? opt.max-cnt : batchSize;
    if (n > 0) {
      do {
        // exit loop if another one has started
        if (!map.timers.initMarkers) return;
        var marker = new opt.markerfunc(new GLatLng(data[cnt][opt.lat], data[cnt][opt.lon]), {icon: getMarkerIcon(map, data[cnt].icon)});
        marker.data = data[cnt];
        GEvent.addListener(marker, 'click', opt.clickfunc);
        map.markers[cnt] = marker;
        cnt++;
      } while(--n);
    }
    if (cnt < opt.max) {
      map.timers.initMarkers = setTimeout(function(){_initMarkers(map, data, opt, ++batchNum, batchSize);}, 0);
    } else {
      delete map.timers.initMarkers;
      setTimeout(function(){map.options.onMarkersInit(map);}, 50);
    }
  }

  function fitToMarkers(map, markers, maxZoom) {
    if (!markers)
      markers = map.markers;
    if (!maxZoom)
      maxZoom = 16;
    var bounds = new GLatLngBounds();
    var len = markers.length;
    for (var i=0; i<len; i++)
      bounds.extend(markers[i].getPoint());
    var zoom = map.map.getBoundsZoomLevel(bounds);
    map.map.setZoom((zoom<maxZoom)?zoom:maxZoom);
    map.map.setCenter(bounds.getCenter());
  }
  
  function updateHistory(map) {
    var center = map.map.getCenter();
    $.address.value(map.options.historyName+'/'+center.lat()+'/'+center.lng()+'/'+map.map.getZoom());
  }

  function loadData(map, feed, type, schema) {
    if (schema)
      map.options.dataSchema = schema;
    $.ajax({url: feed, type: 'GET', dataType: type,
      success: function(data) {
        map.markersLoaded = true;
        map.options.onDataLoad(map, data, type);
      },
      error: function(xhr, status, err) {
          map.options.raiseError(getError(map, HiveMap.ERROR_LOAD_DATA));
      }
    });
  }

  function getError(map, code) {
    return {
      code: code,
      message: map.options.messages.errors[code],
      map: map
    };
  }
  
  function addMarkers(map, markers) {
    for (var i=markers.length; i--;) {
      map.map.addOverlay(markers[i]);
    }
  }

  function getMarkerIcon(map, type) {
    if (!type || !map.options.markerIcons[type])
      type = 'default';
    var opt = map.options.markerIcons[type];
    if (!opt) {
      var icon = new GIcon(G_DEFAULT_ICON);
    } else if (typeof opt == 'object' && opt.constructor == GIcon.prototype.constructor) {
        return opt;
    } else {
      var  icon = new GIcon(G_DEFAULT_ICON, map.options.imagesBase+opt.icon.url);
      icon.iconSize = new GSize(opt.icon.width, opt.icon.height);
      if (opt.shadow) {
        icon.shadow = map.options.imagesBase+opt.shadow.url;
        icon.shadowSize = new GSize(opt.shadow.width, opt.shadow.height);
      } else {
        icon.shadow = null;
      }
      icon.iconAnchor = new GPoint(opt.iconAnchor.x, opt.iconAnchor.y);
      icon.infoWindowAnchor = new GPoint(opt.infoWindowAnchor.x, opt.infoWindowAnchor.y);
      icon.transparent = opt.transparent;
    }
    map.options.markerIcons[type] = icon;
    return icon;
  }

  function checkMarkerOverlap(map, marker) {
    var icon = marker.getIcon();
    var ll = marker.getLatLng();
    var sw = ll;
    var p = map.map.fromLatLngToDivPixel(sw);
    var ne = map.map.fromDivPixelToLatLng(new GPoint(p.x+icon.iconSize.width, p.y+icon.iconSize.height));
    var width = Math.abs(sw.lat()-ne.lat()) / 1.5;
    var height = Math.abs(sw.lng()-ne.lng()) / 1.5;
    var bounds = new GLatLngBounds( // bounds of marker icon plus a little extra
      new GLatLng(ll.lat()-width, ll.lng()-height),
      new GLatLng(ll.lat()+width, ll.lng()+height)
    );
    var list = [];
    for (var i=map.markers.length; i--;) {
      if (bounds.containsLatLng(map.markers[i].getLatLng()))
        list.push(map.markers[i]);
    }
    for (var i=list.length; i--;) {
      if (list[i] == marker) {
        list.splice(i, 1);
        break;
      }
    }
    list.reverse();
    list.unshift(marker);
    (list.length>1) ? map.options.onMarkerClickOverlap(map, marker, list) : map.options.onMarkerClick(map, marker);
  }
  
  function getMarkersInView(map) {
      if (map.markersLoaded) {
        map.markersInView = getMarkersInBounds(map.markers, map.map.getBounds()); 
      }
      return map.markersInView;
  }
  
  function getMarkersInBounds(markers, bounds) {
    var list = [];
    var len = markers.length;
    for (var i=0; i<len; i++) {
      if (bounds.containsLatLng(markers[i].getPoint()))
        list.push(markers[i]);
    }
    return list;
  }
    
  function updateMap(map, lat, lon, zoom) {
    lat = parseFloat(lat);
    lon = parseFloat(lon);
    zoom = parseInt(zoom);
    map.map.closeInfoWindow();
    if (!isNaN(lat) && !isNaN(lon)) {
      map.map.setCenter(new GLatLng(lat, lon), (zoom)?zoom:null);
    } else if (!isNaN(zoom) && zoom!=null) {
      map.map.setZoom(zoom);
    }
  }
  
  function searchMap(map, query, successCallback, errorCallback) {
    if (!map.options.geocoder)
      map.options.raiseError(getError(map, HiveMap.ERROR_NO_GEOCODER));
    map.options.geocoder.getLocations(query, SOCIALHIVE.utils.ajaxUniqueCallback('hivemapSearchMap',
      function(result) {
        if (result.Status.code == G_GEO_SUCCESS) {
          var p = result.Placemark[0].Point.coordinates;
          updateMap(map, p[1], p[0], map.options.geocoder.getZoomFromResult(result));
          if (typeof successCallback == 'function')
            successCallback(map, result);
        } else {
          (typeof errorCallback == 'function') ? 
            errorCallback(map, result) : map.options.raiseError(getError(map, HiveMap.ERROR_SEARCH_FAILED));
        }
      }
    ));
  }
  
  
  // init
  jQuery(function($) {
    if (window.SOCIALHIVE.geo && window.SOCIALHIVE.geo.geocoder)
      HiveMap.defaultOptions.geocoder = window.SOCIALHIVE.geo.geocoder;
    $(window).unload(function(){
      GUnload();
    });
  });
  
  if (!window.SOCIALHIVE)
    window.SOCIALHIVE = {};
  window.SOCIALHIVE.map = HiveMap;
})(jQuery);

